#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "nyu_array.h"
#include "nyu_string.h"

NyuObject *nyu_array_proto = NULL;

static long nyu_array_proto_val = 0;
static NyuString *strval_length;
static NyuString *strval_push;
static NyuString *strval_pop;
static NyuString *strval_unshift;
static NyuString *strval_shift;
static NyuString *strval_getitem;

static void nyu_array_destroy(NyuArray *self);

static NyuString *to_string(NyuObject *self);
static guint get_hash_code(NyuObject *self);
static gboolean equals(NyuObject *self, NyuObject *other);

static long nyu_array_length_fn(long argc, long *argv);
static long nyu_array_push(long argc, long *argv);
static long nyu_array_pop(long argc, long *argv);
static long nyu_array_unshift(long argc, long *argv);
static long nyu_array_shift(long argc, long *argv);
static long nyu_array_getitem(long argc, long *argv);

void nyu_array_initialize()
{
    if (nyu_array_proto)
        return;

    strval_length = nyu_string_new("length");
    strval_push = nyu_string_new("push");
    strval_pop = nyu_string_new("pop");
    strval_unshift = nyu_string_new("unshift");
    strval_shift = nyu_string_new("shift");
    strval_getitem = nyu_string_new("__getitem__");

    nyu_array_proto = nyu_object_new();
    nyu_object_put_slot(nyu_array_proto, NYU_OBJECT(strval_length),
                        ((long)nyu_array_length_fn | 1));
    nyu_object_put_slot(nyu_array_proto, NYU_OBJECT(strval_push),
                        ((long)nyu_array_push | 1));
    nyu_object_put_slot(nyu_array_proto, NYU_OBJECT(strval_pop),
                        ((long)nyu_array_pop | 1));
    nyu_object_put_slot(nyu_array_proto, NYU_OBJECT(strval_unshift),
                        ((long)nyu_array_unshift | 1));
    nyu_object_put_slot(nyu_array_proto, NYU_OBJECT(strval_shift),
                        ((long)nyu_array_shift | 1));
    nyu_object_put_slot(nyu_array_proto, NYU_OBJECT(strval_getitem),
                        ((long)nyu_array_getitem | 1));

    nyu_array_proto_val = ((long)nyu_array_proto | 2);
}

void nyu_array_finalize()
{
    if (!nyu_array_proto)
        return;

    nyu_refcnt_unref(nyu_array_proto);
    nyu_array_proto = NULL;
    nyu_array_proto_val = 0;
    strval_length = NULL;
    strval_push = NULL;
    strval_pop = NULL;
    strval_unshift = NULL;
    strval_shift = NULL;
}

NyuArray *nyu_array_new()
{
    NyuArray *self;
    NyuObject *obj;

    self = nyu_refcnt_new(NyuArray);
    if (!self) {
        perror("cannot alloc NyuObject!!");
        abort();
    }

    obj = NYU_OBJECT(self);

    NYU_OBJECT_INIT(obj, NYU_OBJECT_TYPE_ARRAY, nyu_array_destroy);

    obj->to_string = to_string;
    obj->get_hash_code = get_hash_code;
    obj->equals = equals;

    nyu_refcnt_ref(nyu_array_proto);
    g_ptr_array_add(obj->slot_array, (gpointer)nyu_array_proto_val);
    g_hash_table_insert(obj->slots, nyu_refcnt_ref(strval_proto),
                        GINT_TO_POINTER(obj->slot_array->len));

    self->array = g_ptr_array_new();

    return self;
}

NyuArray *nyu_array_sized_new(long len)
{
    NyuArray *self;
    NyuObject *obj;

    self = nyu_refcnt_new(NyuArray);
    if (!self) {
        perror("cannot alloc NyuObject!!");
        abort();
    }

    obj = NYU_OBJECT(self);

    NYU_OBJECT_INIT(obj, NYU_OBJECT_TYPE_ARRAY, nyu_array_destroy);

    obj->to_string = to_string;
    obj->get_hash_code = get_hash_code;
    obj->equals = equals;

    nyu_refcnt_ref(nyu_array_proto);
    g_ptr_array_add(obj->slot_array, (gpointer)nyu_array_proto_val);
    g_hash_table_insert(obj->slots, nyu_refcnt_ref(strval_proto),
                        GINT_TO_POINTER(obj->slot_array->len));

    self->array = g_ptr_array_sized_new(len);

    return self;
}

static void nyu_array_destroy(NyuArray *self)
{
    if (!self)
        return;

    g_ptr_array_free(self->array, TRUE);

    nyu_object_destroy(NYU_OBJECT(self));
}

static NyuString *to_string(NyuObject *self)
{
    return nyu_string_new("TEST");
}

static guint get_hash_code(NyuObject *self)
{
    return (guint)((long)self);
}

static gboolean equals(NyuObject *self, NyuObject *other)
{
    return (self == other);
}

static long nyu_array_length_fn(long argc, long *argv)
{
    NyuArray *self = NYU_ARRAY(argv[0] & ~3);

    if (NYU_OBJECT(self)->type != NYU_OBJECT_TYPE_ARRAY)
        return 0;

    return self->array->len << 2;
}

static long nyu_array_push(long argc, long *argv)
{
    NyuArray *self = NYU_ARRAY(argv[0] & ~3);
    long i;

    if (NYU_OBJECT(self)->type != NYU_OBJECT_TYPE_ARRAY)
        return 0;

    for (i = 1; i < argc; i++) {
        g_ptr_array_add(self->array, (gpointer)argv[i]);
    }

    return 0;
}

static long nyu_array_pop(long argc, long *argv)
{
    NyuArray *self = NYU_ARRAY(argv[0] & ~3);
    long val;

    if (NYU_OBJECT(self)->type != NYU_OBJECT_TYPE_ARRAY)
        return 0;

    if (self->array->len <= 0)
        return 0;

    val = (long)g_ptr_array_index(self->array, self->array->len - 1);
    g_ptr_array_remove_index_fast(self->array, self->array->len - 1);

    return val;
}

static long nyu_array_unshift(long argc, long *argv)
{
    NyuArray *self = NYU_ARRAY(argv[0] & ~3);
    long i;

    if (NYU_OBJECT(self)->type != NYU_OBJECT_TYPE_ARRAY)
        return 0;

    /* slow */
    g_ptr_array_set_size(self->array, self->array->len + argc - 1);
    g_memmove(self->array->pdata + argc - 1, self->array->pdata,
              self->array->len);

    for (i = 1; i < argc; i++) {
        g_ptr_array_index(self->array, i-1) = (gpointer)argv[i];
    }

    return 0;
}

static long nyu_array_shift(long argc, long *argv)
{
    NyuArray *self = NYU_ARRAY(argv[0] & ~3);
    long val;

    if (NYU_OBJECT(self)->type != NYU_OBJECT_TYPE_ARRAY)
        return 0;

    if (self->array->len <= 0)
        return 0;

    /* slow */
    val = (long)g_ptr_array_index(self->array, 0);
    g_ptr_array_remove_index(self->array, 0);

    return val;
}

static long nyu_array_getitem(long argc, long *argv)
{
    NyuArray *self = NYU_ARRAY(argv[0] & ~3);
    long idx;

    if (NYU_OBJECT(self)->type != NYU_OBJECT_TYPE_ARRAY)
        return 0;

    idx = argv[1] >> 2;

    if (self->array->len <= 0 || self->array->len <= idx)
        return 0;

    return (long)g_ptr_array_index(self->array, idx);
}
